package com.vinjogames.util;

import java.util.HashMap;

import android.graphics.PointF;
import android.util.FloatMath;

import com.vinjogames.entities.GameMap;
import com.vinjogames.entities.GameObject;
import com.vinjogames.entities.Projectile;
import com.vinjogames.entities.Turret;
import com.vinjogames.entities.Unit;
import com.vinjogames.gfx.AnimationManager;
import com.vinjogames.gfx.Camera;

/**
 * 
 * Projectile motion physics engine + collision detection.
 * 
 * @author Joey Jones
 * 
 */

public class Physics {
	
	private static Physics phys;
	
	//Static objects to avoid Java GC calls.
	private static float a = 0.0f;
	private static int i = 0;
	private static int j = 0;
	private static int k = 0;
	private static int m = 0;
	private static float r = 0.0f;
	private static PointF objLocation;
	private static PointF pointOfImpact;
	private static PointF randomPoint;
	private static LineSegmentF segmentOfImpact = new LineSegmentF();
	private static PointF dest = new PointF();
	private static VectorF v1 = new VectorF();
	private static VectorF v2 = new VectorF();

	//Public statics.
	public static final float GRAVITY = -9.800f;
	public static final float G = GRAVITY;
	public static float WIND = 0.0f;
	
	//Private members.
	private PointFFlagged[] terrainPoints;
	private GameObject[] collidableObjects;
	private LineSegmentF[] mapLines;
	private Projectile[] projectiles;
	private float leftoverTime;
	private boolean performing;
	
	private GameMap gameMap;
	
	public Physics(int players, GameMap map) {
		gameMap = map;
		projectiles = new Projectile[GameConVars.MAX_PROJECTILES];
		collidableObjects = new GameObject[GameConVars.MAX_UNIT_COUNT_PER_PLAYER * players + map.getTurretCount() + map.getResourceCount()];
		terrainPoints = map.getHeightMap();
		generateMapLines();
		leftoverTime = 0;
		pointOfImpact = new PointF();
		objLocation = new PointF();
		randomPoint = new PointF();
		performing = false;
	}
	
	public static Physics getInstance() {
		if(phys != null)
			return phys;
		else return phys = new Physics(GameConVars.DEFAULT_PLAYERS, GameMap.getDefaultMap());
	}
	
	public static Physics getInstance(int players, GameMap map) {
		if(phys != null)
			phys.resetEngine(players, map);
		else
			phys = new Physics(players, map);
		return phys;
	}
	
	public void resetEngine(int players, GameMap map) {
		gameMap = map;
		projectiles = new Projectile[GameConVars.MAX_PROJECTILES];
		collidableObjects = new GameObject[GameConVars.MAX_UNIT_COUNT_PER_PLAYER * players + map.getTurretCount() + map.getResourceCount()];
		terrainPoints = map.getHeightMap();
		generateMapLines();
		leftoverTime = 0;
		performing = false;
		WIND = 0.0f;
	}
	
	private void generateMapLines() {
		if(terrainPoints.length <= 0) Logger.log("VJPhysics", "There weren't any terrain points!");

		if(mapLines == null) mapLines = new LineSegmentF[terrainPoints.length];
		
		synchronized(mapLines){
			for(int n = 0; n < mapLines.length && n < terrainPoints.length - 1; ++n) {
				if(mapLines[n] == null) mapLines[n] = new LineSegmentF();
				mapLines[n].setStart(terrainPoints[n]);
				mapLines[n].setEnd(terrainPoints[n+1]);
			}
		}
	}
	
	public void addProjectile(Projectile p) {
		for(m = 0; m < projectiles.length; ++m) {
			if(projectiles[m] == null) {
				projectiles[m] = p;
				performing = true;
				return;
			}
		}
	}
	
	public void addCollidableObject(GameObject o) {
		for(m = 0; m < collidableObjects.length; ++m) {
			if(collidableObjects[m] == null) {
				collidableObjects[m] = o;
				return;
			}
		}
	}
	
	private void releaseProjectile(Projectile p) {
		Logger.log("VJPhysics", "Projectile " + p + " has been deleted.");
		if(p == null) return;
		for(m = 0; m < projectiles.length; ++m)
			if(projectiles[m] != null && projectiles[m].equals(p)) {
				p.deactivateObject();
				MemoryManager.getInstance().releaseProjectile(p);
				Camera.getInstance().clearProjectileFocus();
				projectiles[m] = null;
			}
	}
	
	private void releaseCollidableObject(GameObject o) {
		Logger.log("VJPhysics", "Object " + o + " has been deleted.");
		if(o == null) return;
		for(m = 0; m < collidableObjects.length; ++m)
			if(collidableObjects[m] != null && collidableObjects[m].equals(o)) {
				o.deactivateObject();
				if(o instanceof Unit)
					MemoryManager.getInstance().releaseUnit((Unit)o);
				else if (o instanceof Turret)
					MemoryManager.getInstance().releaseTurret((Turret)o);
				collidableObjects[m] = null;
			}
	}
	
	/**
	 * Randomly changes the wind.
	 */
	public void updateWind() {
		WIND += (2.00001f * GameConVars.MAX_WIND_CHANGE * Math.random()) - GameConVars.MAX_WIND_CHANGE; 
		if(Math.abs(WIND) > GameConVars.MAX_WIND)
			if(WIND < 0) WIND = -1.0f * GameConVars.MAX_WIND;
			else WIND = GameConVars.MAX_WIND;
	}
	
	
	
	/**
	 * Calculates the position and collision points of all projectiles.
	 * 
	 * @param t The time since the last tick.
	 */
	public boolean tick(long t) {
		
		//Execute a physics tick every set fraction of a second based on the max DPS.
		if(t <= 0) return performing;
		else if(leftoverTime + t < (1000.0f / GameConVars.FPS_MAX)) {
			leftoverTime += t;
			return performing;
		}
		
		//Get our t in seconds divided by slow factor.
		float time = (leftoverTime + t) / (1000.0f * GameConVars.TIME_SLOW_FACTOR);
		leftoverTime = 0;
		performing = false;
		for(i = 0; i < projectiles.length; ++i) {
			checkProjectileOnBoundaries(projectiles[i]);
			if(projectiles[i] != null) {
				performing = true;
			//Prequel - Do special attack checks in case the projectile needs to make some sort of move.
				//i.e.	multi - Breaks into multiple shells in the air.
				//		flak - Causes AOE damage at X distance from parent weapon.
				//		etc.

			//FOR REGULAR PROJECTILES:
				//I. Check projectile collision with objects.
				if((checkCollisionObjects(projectiles[i], time)) != null) {
					Logger.log("VJPhysics", "An object collision occurred!");
					Logger.popUp("An object collision occurred!" + projectiles[i].getLocation().x + ":" + projectiles[i].getLocation().y);
					//a. Call damage calculation around point of impact.
					HandleObjectCollision(projectiles[i], pointOfImpact);
				}
			//II. Check projectile collision with the ground.
				else if((checkCollisionGround(projectiles[i], time)) != null) {
					Logger.log("VJPhysics", "Ground collision occurred!");
					Logger.popUp("Ground collision occurred!" + projectiles[i].getLocation().x + ":" + projectiles[i].getLocation().y);
					
					//a. Call damage calculation around point of impact.
					HandleGroundCollision(pointOfImpact, segmentOfImpact, projectiles[i]);
				}
			//III. If there's no collision, move projectile to new position
				else {
					projectiles[i].updateLocation(time);
					//Logger.log("Location: (" + projectiles[i].getLocation().x + ", " + projectiles[i].getLocation().y + ")");
					pointOfImpact = null;
				}
				
				//TODO: AI Needs to hold a preliminary shot and then be passed the POI to complete the shot data.
				/*if(pointOfImpact != null) {
					Player p = game.getCurrentPlayer();
					if(p.getType() == GameConVars.PLAYER_AI) {
						p.myAI.finishLoggingShot(pointOfImpact);
					}
				}*/
			}
		}
		return performing;
	}
	
	private void checkProjectileOnBoundaries(Projectile p) {
		if(p == null) return;
		
		if(p.getLocation().x < 0 || p.getLocation().x > gameMap.getSize() || p.getLocation().y < 0) {
			releaseProjectile(p);
		}
	}

	private PointF checkCollisionObjects(Projectile p, float time) {
		if(p == null) throw new NullPointerException();
		pointOfImpact = null;		
		for(j = 0; j < collidableObjects.length; ++j) {
			GameObject obj = collidableObjects[j];
			if(obj != null && !obj.equals(p.getFiredBy())) {
				//1. Check projectile p against bounding circle.
				r = GameConVars.PHYSICS_CIRCLE_RADIUS;//collidableObjects[j].getHitRadius();
				objLocation.set(obj.getLocation().x - obj.getGLWidth() * 0.5f, obj.getLocation().y + obj.getGLHeight());
				//2. If it collided with the bounding circle:
				//if(CollidesWithCircle(p.getVectorToNewLocation(time), p.getLocation(), r, objLocation)) {
				//	Logger.log("VJPhysics", "Bounding circle check passed, checking object hitbox");
					//a. Check projectile p against collision array.
					pointOfImpact = checkCollisionArray(p, obj.getBasicCollisionLines(), time); //TODO: Switch back to getCollisionLines() and fix collisionLines.
					
					//if(pointOfImpact == null) Logger.log("VJPhysics", "Object collision did not occur.");
					//b. Return point of impact if collision array hit.
					if(pointOfImpact != null)
					{
						//TODO: Temporary code to test unit damage. Move this to handle object collision at a later date.
						obj.takeDamage(0.5f);
						//This is still being moved but this is where objects die.
						if(obj.getHealth() <= 0.0f){
							unfreezeMapIfDeadIsStructure(obj);
							releaseCollidableObject(obj);
							collidableObjects[j] = null;
						}
						return pointOfImpact;
					}
				//}
			}
		}
		//3. Return null if we finish the loop.
		return null;
	}
	
	private void unfreezeMapIfDeadIsStructure(GameObject o) {
		switch(o.getType()) {
		case GameConVars.OBJECT_UNIT:
		case GameConVars.OBJECT_MISC:
			break;
		case GameConVars.OBJECT_BASE:
			gameMap.structureDestroyedAtX(o.getLocation().x, GameConVars.MAP_BASE_WIDTH);
			break;
		case GameConVars.OBJECT_RESOURCE:
			gameMap.structureDestroyedAtX(o.getLocation().x, GameConVars.MAP_RESOURCE_WIDTH);
			break;
		case GameConVars.OBJECT_TURRET:
			gameMap.structureDestroyedAtX(o.getLocation().x, GameConVars.MAP_TURRET_WIDTH);
			break;
		}
	}
	
	private PointF checkCollisionGround(Projectile p, float time) {
		pointOfImpact = null;
		segmentOfImpact = null;
		return checkCollisionArray(p, mapLines, time);
	}
	
	private PointF checkCollisionArray(Projectile p, LineSegmentF[] arr, float time) {
		if(p == null || arr == null) throw new NullPointerException();
		
		for(k = 0; k < arr.length - 1; ++k) {
			//Perform line segment collision.
			
			objLocation.set(p.getLocation());
			dest.set(p.getNewLocation(time));
			
			//FIRST LINE
			double x1 = arr[k].getStart().getX();
			double y1 = arr[k].getStart().getY();
			double x2 = arr[k].getEnd().getX();
			double y2 = arr[k].getEnd().getY();
			
			//SECOND LINE (need to convert projectile into a line segment)
			double x3 = objLocation.x;
			double y3 = objLocation.y;
			double x4 = dest.x;
			double y4 = dest.y;
			
			double r = ( (y1 - y3) * (x4 - x3) - (x1 - x3) * (y4 - y3) ) /
					  ( (x2 - x1) * (y4 - y3) - (y2 - y1) * (x4 - x3) );
			
			double s = ( (y1 - y3) * (x2 - x1) - (x1 - x3) * (y2 - y1) ) /
					  ( (x2 - x1) * (y4 - y3) - (y2 - y1) * (x4 - x3) );
			
			if(r >= 0.0 && r <= 1.0 && s >= 0.0 && s <= 1.0) {
				double tempx = x1 + r * (x2 - x1);
				double tempy = y1 + r * (y2 - y1);
				Logger.log("VJPhysics", "Collided with a line segment array.");
				randomPoint.set((float)tempx, (float)tempy);
				pointOfImpact = new PointF(randomPoint.x, randomPoint.y);
				return randomPoint;
			}			
		}
		return null;
	}
		
	private void HandleGroundCollision(PointF pOI, LineSegmentF lineSeg, Projectile p) {
		SoundManager.getInstance().PlaySound(SoundManager.getInstance().BOMB_SOUND_ID);//TODO
		AnimationManager.getInstance().startFrameAnimation(GameConVars.ANIM_EXPLOSION2, 1500, false, pOI.x, pOI.y);
		
		AnimationManager.getInstance().startFrameAnimation(GameConVars.ANIM_EXPLOSION3, 500, false, pOI.x + 1, pOI.y);
		AnimationManager.getInstance().startFrameAnimation(GameConVars.ANIM_EXPLOSION4, 500, false, pOI.x -1, pOI.y);
		AnimationManager.getInstance().startFrameAnimation(GameConVars.ANIM_SPARK2, 400, false, pOI.x, pOI.y + 1);
		
		//AnimationManager.getInstance().startFrameAnimation(GameConVars.ANIM_FLAME, 750, true, pOI.x, pOI.y);
		
		switch(p.getAttackType()) {
		case Projectile.BOUNCE:
			break;
		case Projectile.SHELL:
		case Projectile.BOMB:
		case Projectile.MISSILE:
		case Projectile.MULTI:
		case Projectile.FLAK:
			gameMap.damageMap(pOI.x, 0.5f);
			releaseProjectile(p);
			break;
		case Projectile.FLAME:
			
			releaseProjectile(p);
			break;
		case Projectile.PIG:
			
			releaseProjectile(p);
			break;
		default:
				break;
		}
		gameMap.mapDamageOccurred();
	}
	
	/**
	 * Calculates the damage of a projectile in an area based on the projectile type.
	 * Does not cause damage to the ground if it hit an object first.
	 * @param p The projectile causing damage
	 * @param poi The point of impact of the projectile
	 */
	private void HandleObjectCollision(Projectile p, PointF poi) {
		//TODO: Make sure to release the projectile if it is finished.
		switch(p.getAttackType()) {
		case Projectile.SHELL: //Shell/Bomb/Missile/Flak/Bounce do damage in a circle from where they land.
		case Projectile.BOMB:
		case Projectile.MISSILE:
		case Projectile.MULTI: //Result of a multi shell hitting something before it can break apart.
		case Projectile.BOUNCE: //Bouncing projectiles ALWAYS detonate on object collision.
		case Projectile.FLAK: //In this case POI is where the flak blew up in the air.
			//1. Calculate affected objects using circular radius.
			//2. Deal damage to all objects in radius.
			
			releaseProjectile(p);
			break;
		case Projectile.FLAME: //If the projectile is flame:
			//1. Calculate affected objects using circular radius.
			//2. Deal damage only to objects that are touching or very near to touching the ground.
			
			releaseProjectile(p);
			break;
		case Projectile.PIG: //If the projectile is a pig:
			//1. Blow up EVERYTHING.
			//2. Start new game
			
			releaseProjectile(p);
			break;
		default:	
			break;
		}
	}
	
	private boolean CollidesWithCircle(VectorF v, PointF point, float r, PointF circle) {
		//Fast check with the circle. (Quick culling)
		
		if(v.magnitude() < Math.abs(LineSegmentF.Length(point, circle) - r)) {
			return false;
		}
		else if(LineSegmentF.Length(point,  circle) <= r) {
			Logger.log("VJPhysics", "Inside of circle with location (" + circle.x + "," + circle.y + ")");
			return true;
		}
		Logger.log("VJPhysics", "Vector would cause collision with circle, doing complicated circle collision check");
		
		//Detailed check with the circle.
		//v1 = D. v2 = delta.
		v1 = v; v2 = VectorF.aMinusB(point, circle);
		//a = (D * delta - sqrt( (D * delta)^2 - |D|*(|delta| - r^2) ) ) / |D|
		a = (v1.times(-1.0f).dot(v2) - FloatMath.sqrt((v1.dot(v2) * v1.dot(v2) - v1.magnitude() * (v2.magnitude() - r * r)))) / v1.magnitude();
		if(a >= 0.0f && a <= 1.0f) return true;
		return false;
	}
	
	/**
	 * 
	 * Calculates the damage dealt by a circular radius attack (Shells, bombs, missiles, multi-shells, bouncers, and flak)
	 * 
	 * @param p
	 * @param poi
	 */
	private void DealDamageCircle(Projectile p, PointF poi) {
		/*
		 * TODO: Grab the radius of attack from the projectile's owner and deal damage to each unit in the returned
		 * HashMap based on its distance from the point of impact. Anything with distance 0 will be full damage and
		 * the damage will taper off based on some linear function.
		 * 
		 * DO NOT DAMAGE THE UNIT THAT FIRED THE PROJECTILE
		 */
		HashMap<GameObject, Float> objectsHit = getAllCollisionPointsInCircle(poi, p.getHitRadius());
	}
	
	/**
	 * 
	 * Calculates the distance from the point of impact for every game object
	 * hit by a blast using a bounding circle.
	 * 
	 * @param poi The point of impact to form a circle around.
	 * 
	 * @param r The radius of the bounding circle.
	 * 
	 * @return A HashMap with GameObjects that were hit by the blast as keys and their nearest point distance from
	 * 		   the blast (including the center point).
	 * 
	 */
	public HashMap<GameObject, Float> getAllCollisionPointsInCircle(PointF poi, float r) {
		HashMap<GameObject, Float> collisionPoints = new HashMap<GameObject, Float>();
		
		for(GameObject obj : collidableObjects) {
			/*  
			 * TODO: Convert collision lines in GameObject to be collision points and then parse through each point
			 * to see if it is contained within the circle. If not, move on. If an object already exists in the map
			 * and it will end up with a 2nd point being hit just modify the existing entry to be 0 distance away
			 * from the impact (meaning it takes full damage). This prevents direct hits against tanks from being
			 * less effective if they collide with the center of a line segment.
			 */
		}
		
		return collisionPoints;
	}
	
	public Projectile[] getProjectiles(){ return projectiles; }
	
	public float getWind(){ return WIND; }
	public void setWind(float w){ WIND = w; }
}
